pragma Ada_2012;
with Ada.Text_IO;
with Ada.Strings.Fixed;
with Ada.Containers.Indefinite_Hashed_Sets;
with Ada.Command_Line;
with String_Utils;
package body Conf4A is

   type Range_Type is record
      From, To : Natural;
   end record;

   function Is_Empty (R : Range_Type) return Boolean
   is
   begin
      return R.From > R.To;
   end Is_Empty;

   procedure Trim (S : String; R : in out Range_Type)
   is
   begin
      R := (S'First, S'Last);
      for i in S'Range loop
         if S(i) in Character'Val(33) .. Character'Last then
            R.From := i;
            exit;
         end if;
      end loop;
      for i in reverse S'Range loop
         if S(i) in Character'Val(33) .. Character'Last then
            R.To := i;
            exit;
         end if;
      end loop;
   end Trim;

   function Is_Quoted (S : String; R : Range_Type) return Boolean
   is
      p : String renames S(R.From .. R.To);
   begin
      if p'Length < 2 then
         return False;
      end if;
      if p(p'First) = '"' and p(p'Last) = '"' then
         return True;
      end if;
      return False;
   end Is_Quoted;

   procedure Clear (this : in out Setting_Type)
   is
   begin
      this.Content.Clear;
      this.Items.Clear;
   end Clear;

   procedure Attach (this : in out Setting_Type; Filename : String := App_Config) 
   is
      use Ada.Command_Line;
   begin
      if Filename'Length /= 0 then
         this.Filename := To_Unbounded_String(Filename);
      else
         this.Filename := To_Unbounded_String(Command_Name & ".conf");
      end if;
   end Attach;

   procedure Load (this : in out Setting_Type)
   is
      use Ada.Text_IO;
      F : File_Type;

      procedure Split_Key_Value (line : String) 
      is
         k, v : Range_Type;
      begin
         for i in line'Range loop
            if line(i) = '=' then
               Trim(line(line'First .. i - 1), k);
               exit when Is_Empty(k);
               exit when line(k.from) = '#';
               Trim(line(i + 1 .. line'Last), v);
               if Is_Quoted(line, v) then
                  v.From := v.From + 1;
                  v.To := v.To - 1;
               end if;
--               exit when Is_Empty(v);
               this.Items.Include(line(k.From .. k.To), line(v.From .. v.To));
               return;
            end if;
         end loop;
      end Split_Key_Value;

   begin
      if Length(This.Filename) = 0 then
         this.Attach(App_Config);
      end if;
      Open(File => F, Name => To_String(this.Filename), Mode => In_File);
      while not End_Of_File(F) loop
         this.Content.Append(Get_Line(F));
      end loop;
      Close(F);
      for line of this.Content loop
         Split_Key_Value(line);
      end loop;
   end Load;

   procedure Save (this : in out Setting_Type) 
   is
      use Ada.Text_IO;
      f : File_Type;
      keys : String_Hash_Sets.Set;

      procedure Update_Content (List_Cursor : String_Lists.Cursor) 
      is
         line : String := String_Lists.Element(List_Cursor);
         changed : Boolean := False;
         p : String_Hash_Sets.Cursor;
         value_is_quoted : Boolean;
         k, v : Range_Type;
      begin
         for i in line'Range loop
            if line(i) /= '=' then
               goto continue;
            end if;
            Trim(line(line'First .. i - 1), k);
            exit when Is_Empty(k);
            exit when line(k.from) = '#';
            Trim(line(i + 1 .. line'Last), v);
            value_is_quoted := Is_Quoted(line, v);
            p := keys.Find(line(k.From .. k.To));
            if not String_Hash_Sets.Has_Element(p) then
               -- comment deleted line
               this.Content.Replace_Element(List_Cursor, "#" & line);
            else
               declare
                  new_value : String := this.Items.Element(line(k.From .. k.To));
               begin
                  if value_is_quoted then
                     this.Content.Replace_Element(
                        List_Cursor, 
                        line(line'First .. v.From) & new_value & '"');
                  else
                     this.Content.Replace_Element(
                        List_Cursor, 
                        line(line'First .. i) & " " & new_value);
                  end if;
               end;
               keys.Delete(p);
            end if;
            return;
            <<continue>>
         end loop;
      end Update_Content;
   begin
      if Length(This.Filename) = 0 then
         this.Attach(App_Config);
      end if;
      -- copy key
      for i in this.Items.Iterate loop
         keys.Include(String_Hash_Maps.Key(i));
      end loop;
      this.Content.Iterate(Update_Content'Access);
      for key of keys loop
         this.Content.Append(key & " = " & this.Items.Element(key));
      end loop;
      Open(File => f, Name => To_String(this.Filename), Mode => Out_File);
      for line of this.Content loop
         Put_Line(f, line);
      end loop;
      Close(f);
   end Save;

   procedure Set (
      This  : in out Setting_Type;
      Key   : String;
      Value : String;
      Save_After_Changed : Boolean := False
      )
   is
   begin
      this.Items.Include(key, value);
      if Save_After_Changed then
         this.Save;
      end if;
   end Set;

   procedure Delete (
      this               : in out Setting_Type;
      Key                : String;
      Save_After_Changed : Boolean := False
      )
   is
      n : Count_Type := this.Items.Length;
   begin
      this.Items.Exclude(key);
      if Save_After_Changed and then n /= this.Items.Length then
         this.Save;
      end if;
   end Delete;

   procedure Delete_Section (
      this               : in out Setting_Type;
      Prefix             : String;
      Save_After_Changed : Boolean := False
      )
   is
      n : Count_Type := this.Items.Length;
      p : String_Hash_Maps.Cursor := this.Items.First;
      q : String_Hash_Maps.Cursor;
   begin
      while String_Hash_Maps.Has_Element(p) loop
         q := String_Hash_Maps.Next(p);
         if String_Utils.Starts_With(String_Hash_Maps.Element(p), prefix) then
            this.Items.Delete(p);
         end if;
         p := q;
      end loop;
      if Save_After_Changed and then n /= this.Items.Length then
         this.Save;
      end if;
   end Delete_Section;

   function Get (
      This          : in Setting_Type;
      Key           : String;
      Default_Value : String := ""
      ) return String
   is
      p : String_Hash_Maps.Cursor := this.Items.Find(key);
   begin
      if String_Hash_Maps.Has_Element(p) then
         return String_Hash_Maps.Element(p);
      else
         return Default_Value;
      end if;
   end Get;

   function Get (
      This          : in Setting_Type;
      Key           : String;
      Default_Value : Integer := 0
      ) return Integer
   is
      p : String_Hash_Maps.Cursor := this.Items.Find(key);
   begin
      if String_Hash_Maps.Has_Element(p) then
         return Integer'Value(String_Hash_Maps.Element(p));
      else
         return Default_Value;
      end if;
   exception
      when others => return Default_Value;
   end Get;

   function Get (
      this          : in Setting_Type;
      Key           : String;
      Default_Value : Duration := 0.0
      ) return Duration
   is
      p : String_Hash_Maps.Cursor := this.Items.Find(key);
   begin
      if String_Hash_Maps.Has_Element(p) then
         return Duration(Integer'Value(String_Hash_Maps.Element(p)));
      else
         return Default_Value;
      end if;
   exception
      when others => return Default_Value;
   end Get;

   function Get (
      this          : in Setting_Type;
      Key           : String;
      Default_Value : Boolean := False
      ) return Boolean
   is
      p : String_Hash_Maps.Cursor := this.Items.Find(key);
   begin
      if String_Hash_Maps.Has_Element(p) then
         return Boolean'Value(String_Hash_Maps.Element(p));
      else
         return Default_Value;
      end if;
   exception
      when others => return Default_Value;
   end Get;

   procedure Read (
      This  : in Setting_Type;
      Key   : String;
      Value : in out Integer
      )
   is
   begin
      Value := this.Get(Key, Value);
   end Read;

   procedure Read (
      This  : in Setting_Type;
      Key   : String;
      Value : in out Duration 
      )
   is
   begin
      Value := this.Get(Key, Value);
   end Read;

   procedure Read (
      This  : in Setting_Type;
      Key   : String;
      Value : in out Boolean 
      )
   is
   begin
      Value := this.Get(Key, Value);
   end Read;

   procedure Read (
      this  : in Setting_Type;
      Key   : String;
      Value : in out Unbounded.Unbounded_String
      )
   is
      v : String := Unbounded.To_String(Value);
   begin
      Value := Unbounded.To_Unbounded_String(this.Get(Key, v));
   end Read;

   procedure Iterate (
      This    : in out Setting_Type;
      Process : not null access procedure (Key, Value : String)
      )
   is
   begin
      for i in this.Items.Iterate loop
         Process(String_Hash_Maps.Key(i), String_Hash_Maps.Element(i));
      end loop;
   end Iterate;

   function Generic_Get (
      This          : in Setting_Type;
      Key           : String;
      Default_Value : Value_Type := Value_Type'First
      ) return Value_Type
   is
      p : String_Hash_Maps.Cursor := this.Items.Find(key);
   begin
      if String_Hash_Maps.Has_Element(p) then
         return Value_Type'Value(String_Hash_Maps.Element(p));
      else
         return Default_Value;
      end if;
   exception
      when others => return Default_Value;
   end Generic_Get;


end Conf4A;
